/*
 * Wiiremote.java
 *
 * Created on December 2, 2007, 11:00 PM
 *
 *  Copyright (C) 2010 Tal Eisenberg
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package io;

import io.WiiControl.Control;
import io.WiiControl.Type;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import tal.drivechain.chain.AbortChainException;
import tal.drivechain.plugin.Channel;
import tal.drivechain.plugin.Module;
import tal.drivechain.variable.types.DataType;
import tal.drivechain.variable.SetProperty.Flag;
import tal.drivechain.variable.VarTableAction;
import tal.drivechain.variable.Variable;
import tal.drivechain.wildcard.Wildcard;
import wiiremotej.WiiRemote;
import wiiremotej.WiiRemoteExtension;
import wiiremotej.NunchukExtension;
import wiiremotej.WiiRemoteJ;
import wiiremotej.event.WRAccelerationEvent;
import wiiremotej.event.WRButtonEvent;
import wiiremotej.event.WRCombinedEvent;
import wiiremotej.event.WRExtensionEvent;
import wiiremotej.event.WRIREvent;
import wiiremotej.event.WRNunchukExtensionEvent;
import wiiremotej.event.WRStatusEvent;
import wiiremotej.event.WiiRemoteDiscoveredEvent;
import wiiremotej.event.WiiRemoteDiscoveryListener;
import wiiremotej.event.WiiRemoteListener;

/**
 *
 * @author Main
 */
public class Wiiremote extends Module implements WiiRemoteDiscoveryListener, WiiRemoteListener {
    // currently missing support for multiple wiimotes, extensions, ir and status info.
    // Wiiremote stores this for each device discovered but will not distinguish from which device
    // the event was received.
    
    private List<Object[]> controlOutput;
    private HashMap<WiiControl, Variable> wiiControlOutput = new HashMap<WiiControl, Variable>();
    private Variable varValue;
    private VarTableAction setValue;
    private Variable varBattery;
    private HashMap<Integer,WiiRemote> wiiremotes = new HashMap<Integer,WiiRemote>();
    
    // only works with 1 wiimote!!
    private boolean hasAcc = false;
    private boolean hasIR = false;
    private boolean hasExtension = false;
    
    @Override
    public void setupProperties() throws IllegalArgumentException, AbortChainException {
        //prevent an error with bluecove 2.1.0 on mac
        System.setProperty("bluecove.jsr82.psm_minimum_off", "true");
        varValue = new Variable("value", DataType.FLOAT, null, true);
        setValue = new VarTableAction("value", VarTableAction.Action.SET_ANY);
        varBattery = new Variable("battery", DataType.FLOAT, null, true);
        if (debugFlag) WiiRemoteJ.setConsoleLoggingAll();
        setControlOutput(props.getProperty("controlOutput"), null);        
    }


    public void setControlOutput(Object value, Flag flag) {
        List<Object[]> newControlOutput = (List<Object[]>)value;
        if (flag == Flag.ADD) {
            if (controlOutput==null) controlOutput = new ArrayList<Object[]>();
            controlOutput.addAll(newControlOutput);
            wiiControlOutput.putAll(parseWiiMatchers(newControlOutput));
            this.updateEnabledControls();
        } else {
            controlOutput = newControlOutput;
            wiiControlOutput = parseWiiMatchers(controlOutput);
            this.updateEnabledControls();
        }
    } public Object getControlOutput() { return controlOutput; }

    @Override
    public void start() {
        super.start();
        log("Looking for Wii remotes...", MSG.INFO);
        try {
            WiiRemoteJ.findRemotes(this, 1);
        } catch (Exception e) {
            throw new IllegalArgumentException("Error while searching for wiiremotes: " + e.getMessage());
        }
    }
    
    public void commandPlay(List<Variable> parameters) {
        if (wiiremotes.get(0)==null || !((WiiRemote)wiiremotes.get(0)).isConnected()) {
            log("Can't send command. Wiiremote " + 0 + " is not connected.", MSG.WARNING);
            return;
        }
        log("play command not supported yet.", MSG.WARNING);        
    }
    
    public void commandMute(List<Variable> parameters) {
        if (wiiremotes.get(0)==null || !((WiiRemote)wiiremotes.get(0)).isConnected()) {
            log("Can't send command. Wiiremote " + 0 + " is not connected.", MSG.WARNING);
            return;
        }
        log("mute command not supported yet.", MSG.WARNING);                            
    }

    public void commandVolume(List<Variable> parameters) {
        if (wiiremotes.get(0)==null || !((WiiRemote)wiiremotes.get(0)).isConnected()) {
            log("Can't send command. Wiiremote " + 0 + " is not connected.", MSG.WARNING);
            return;
        }
        if (parameters.size()!=1)
            log("Bad syntax for command volume{vol(double)}", MSG.WARNING);
        double vol = (Double)DataType.FLOAT.cast(parameters.get(0).getValue(this));
        try {
            wiiremotes.get(0).setSpeakerVolume(vol);
        } catch (Exception ex) {
            log("While setting volume: " + ex.getMessage(), MSG.WARNING);
        }
    }
    
    public void commandBattery(List<Variable> parameters) {
        if (wiiremotes.get(0)==null || !((WiiRemote)wiiremotes.get(0)).isConnected()) {
            log("Can't send command. Wiiremote " + 0 + " is not connected.", MSG.WARNING);
            return;
        }
        
        if (debugFlag) log("Sending battery status.", MSG.DEBUG);
        try {
            ((WiiRemote)wiiremotes.get(0)).requestStatus();
        } catch (IOException ex) {
            log("Error while requesting for status report: " + ex.getMessage(), MSG.WARNING);
        }
    }
    
    public void commandStop(List<Variable> parameters) {
        if (wiiremotes.get(0)==null || !((WiiRemote)wiiremotes.get(0)).isConnected()) {
            log("Can't send command. Wiiremote " + 0 + " is not connected.", MSG.WARNING);
            return;
        }
        log("stop command not supported yet.", MSG.WARNING);
    }
    
    public void commandLeds(List<Variable> parameters) {
        if (wiiremotes.get(0)==null || !((WiiRemote)wiiremotes.get(0)).isConnected()) {
            log("Can't send command. Wiiremote " + 0 + " is not connected.", MSG.WARNING);
            return;
        }
        
        //format string:leds,int:led#,bool:state...int:led#,bool:state
        int cSize = parameters.size();
        for (int i=0; i<cSize; i+=2) {
            Object ledIdx = parameters.get(i).getValue(this);
            if (i+1>=cSize) {
                log("Each LED number int must be followed with a LED state boolean var.", MSG.WARNING);
                return;
            }
            Object ledState = parameters.get(i+1).getValue(this);
            if (!(ledIdx instanceof Integer) || !(ledState instanceof Boolean)) {
                log("Invalid command. leds command variable should be in the format of: string:leds,int:led.num,bool:led.state,...", MSG.WARNING);                            return;
            } else {
                try {
                    if (debugFlag) log("Setting LED " + (Integer)ledIdx + " to " + ((Boolean)ledState?"on":"off"), MSG.DEBUG);
                    ((WiiRemote)wiiremotes.get(0)).setLEDIlluminated((Integer)ledIdx, (Boolean)ledState);
                } catch (IllegalArgumentException ex) {
                    ex.printStackTrace();
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
            }
        }
    }
    
    public void commandVibrate(List<Variable> parameters) {
        if (wiiremotes.get(0)==null || !((WiiRemote)wiiremotes.get(0)).isConnected()) {
            log("Can't send command. Wiiremote " + 0 + " is not connected.", MSG.WARNING);
            return;
        }
        
        if (parameters.size()==1) {
            Variable param = parameters.get(0);
            if (param.getDataType().matches(DataType.LONG)) {
                try {
                    ((WiiRemote)wiiremotes.get(0)).vibrateFor((Long)param.getValue(this));
                } catch (IOException ex) {
                    log("Error while trying to vibrate: " + ex.getMessage(), MSG.WARNING);
                }
            } else if (param.getDataType().matches(DataType.BOOLEAN)) {
                boolean val = (Boolean)param.getValue(this);
                try {
                    if (val) 
                        ((WiiRemote)wiiremotes.get(0)).startVibrating();
                    else ((WiiRemote)wiiremotes.get(0)).stopVibrating();
                } catch (IOException ex) {
                    log("Error while trying to change vibration test: " + ex.getMessage(), MSG.WARNING);
                }
            } else {
                log("Invalid vibrate command. vibrate command variable should be (vibrate)[long:duration/bool:state] ", MSG.WARNING);                             
            }
        } else {
            log("Invalid vibrate command. vibrate command variable should be (vibrate)[long:duration/bool:state] ", MSG.WARNING);
        }
    }
    
    public void wiiRemoteDiscovered(WiiRemoteDiscoveredEvent wEvt) {
        log("WiiRemote(" + wEvt.getNumber() + ") found.", MSG.ANOUNCE);
        wEvt.getWiiRemote().addWiiRemoteListener(this);
        try {
            wEvt.getWiiRemote().setAccelerometerEnabled(hasAcc);
            log("Motion sensors are " + (hasAcc?"enabled":"disabled"), MSG.INFO);
        } catch (Exception ex) {
            log("Can't enable/disable motion sensors: " + ex.getMessage(), MSG.WARNING);
        }
        try {
            wEvt.getWiiRemote().setIRSensorEnabled(hasIR, WRIREvent.BASIC);
            log("IR sensor is " + (hasIR?"enabled":"disabled"), MSG.INFO);            
        } catch (Exception ex) {
            log("Can't enable/disable IR sensors: " + ex.getMessage(), MSG.WARNING);
        }        
        wiiremotes.put(wEvt.getNumber(), wEvt.getWiiRemote());
    }

    @Override
    public void stop() {
        WiiRemoteJ.stopFind();
        for (WiiRemote remote : wiiremotes.values()) {
            log("Disconnecting WiiRemote " + remote.getBluetoothAddress() + "...", MSG.INFO);
            remote.disconnect();
            remote.removeWiiRemoteListener(this);

        }
    }

    public void findFinished(int i) {
    }

    public void buttonInputReceived(WRButtonEvent bEvt) {
        for (WiiControl control : wiiControlOutput.keySet()) {
            Double value = control.matches(bEvt, this);
            if (value!=null) {
                defineLocalVars(value);
                if (infoFlag) log("Registered button (" + WiiControl.buttonIntToID(control.buttonId) + ") was " + (value==1?"pressed":"released"), MSG.INFO);
                this.sendToOutputs(wiiControlOutput.get(control), -1);
            }
            
        }
    }

  
    public void statusReported(WRStatusEvent evtStatus) {
        varBattery.setValue(evtStatus.getBatteryLevel(), true);
        sendToOutputs(varBattery, -1);        
    }

    public void accelerationInputReceived(WRAccelerationEvent aEvt) {
        //go over every trigger and see if they need anything
        for (WiiControl control : wiiControlOutput.keySet()) {
            Double value = control.matches(aEvt, this);
            if (value!=null) {
                defineLocalVars(value);
                if (infoFlag) log("Received registered motion event: " + control.id.id + "=" + value, MSG.INFO);
                this.sendToOutputs(wiiControlOutput.get(control), -1);
            }
        }
    }

    public void IRInputReceived(WRIREvent irEvt) {
        for (WiiControl control : wiiControlOutput.keySet()) {
            Double value = control.matches(irEvt, this);
            if (value!=null) {
                defineLocalVars(value);
                if (infoFlag) log("Received registered ir event: " + control.id.id + "=" + value, MSG.INFO);
                this.sendToOutputs(wiiControlOutput.get(control), -1);
            }
        }
    }

    public void extensionInputReceived(WRExtensionEvent wRExtensionEvent) {
        // currently supports nunchuck only
        WRNunchukExtensionEvent evt = (WRNunchukExtensionEvent)wRExtensionEvent;        
        for (WiiControl control : wiiControlOutput.keySet()) {
            Double value = control.matches(evt, this);
            if (value!=null) {
                defineLocalVars(value);
                if (infoFlag) log("Received registered nunchuck event: " + control.id.id + "=" + value, MSG.INFO);
                this.sendToOutputs(wiiControlOutput.get(control), -1);
            }
        }
    }

    public void extensionConnected(WiiRemoteExtension wiiRemoteExtension) {
        try {
            if (hasExtension) {
                if (debugFlag) log("Extension connected: " + wiiRemoteExtension, MSG.DEBUG);
                wiiremotes.get(0).setExtensionEnabled(hasExtension);
                wiiremotes.get(0).setExtensionPayload(wiiRemoteExtension.getPayload());
                if (!(wiiRemoteExtension instanceof NunchukExtension)) {
                    log("Unsupported extension: " + wiiRemoteExtension, MSG.WARNING);
                    wiiremotes.get(0).setExtensionEnabled(false);
                }
            }
            
        } catch (Exception ex) {
            log("Can't enable/disable remote Extension: " + ex.getMessage(), MSG.WARNING);
        } 
        
    }

    public void extensionPartiallyInserted() {
        log("Wiiremote extension only partiall inserted", MSG.WARNING);
    }

    public void extensionUnknown() {
        log("Unknown Wiiremote extension connected", MSG.WARNING);
    }

    public void extensionDisconnected(WiiRemoteExtension wiiRemoteExtension) {
        log("Wiiremote extension is disconnected", MSG.WARNING);
    }

    public void combinedInputReceived(WRCombinedEvent wRCombinedEvent) {
    }

    public void disconnected() {
        System.out.println("received: disconnected");        
    }

    private void defineLocalVars(double value) {
        varValue.setValue(value, true);
        setValue.set(varValue, localVars, this);
    }

    @Override
    protected void msgReceived(Variable in, long timestamp, Channel channel) {
    }

    private HashMap<WiiControl, Variable> parseWiiMatchers(List<Object[]> controlOutput) {
        // value is list( Object[]( Object[](int[](typdId,controlId), wildcard), variable ) )
        // saved as WiiControl -> variable

        HashMap<WiiControl, Variable> ret = new HashMap<WiiControl, Variable>();
        for (Object[] item : controlOutput) {
            Object[] c = (Object[])item[0];
            int[] controlIds = (int[])c[0];
            Wildcard w = (Wildcard)c[1];
            WiiControl control = new WiiControl(controlIds[0], controlIds[1], w);
            ret.put(control, (Variable)item[1]);
        } return ret;
    }

    private void updateEnabledControls() {
        hasExtension = false;
        hasAcc = false;
        hasIR = false;

        for (WiiControl control : wiiControlOutput.keySet()) {
            if (hasExtension == true && hasAcc == true && hasIR == true) break;

            if (control.type==WiiControl.Type.EXTENSION) {
                hasExtension = true;
                if (control.id==WiiControl.Control.NUNCHUK_XACC ||
                        control.id==WiiControl.Control.NUNCHUK_YACC ||
                        control.id==WiiControl.Control.NUNCHUK_ZACC ||
                        control.id==WiiControl.Control.NUNCHUK_PITCH ||
                        control.id==WiiControl.Control.NUNCHUK_ROLL)
                    hasAcc = true;
            } else if (control.type==WiiControl.Type.IR)
                hasIR = true;
            else if (control.type==WiiControl.Type.MOTION)
                hasAcc = true;
        }

        if (wiiremotes!=null) {
            for (WiiRemote r: wiiremotes.values()) {
                if (hasExtension && !r.isExtensionEnabled()) {
                    try { r.setExtensionEnabled(true); } catch (IOException ie) { throw new IllegalArgumentException("While enabling wii extensions: " + ie.getMessage()); }
                } if (hasAcc && !r.isAccelerometerEnabled()) {
                    try { r.setAccelerometerEnabled(true); } catch (IOException ie) { throw new IllegalArgumentException("While enabling wii accelerometer: " + ie.getMessage()); }
                } if (hasIR && !r.isIRSensorEnabled()) {
                    try { r.setIRSensorEnabled(true, WRIREvent.BASIC); } catch (IOException ie) { throw new IllegalArgumentException("While enabling wii IR sensor: " + ie.getMessage()); }
                }
            }
        }


    }

    public static String wiiControlToString(Object prop, HashMap attributes) throws IllegalArgumentException {
        int[] rep = (int[])prop;
        Type type = WiiControl.ordinalToType(rep[0]);

        if (type == Type.BUTTON)
            return "(button)" + WiiControl.buttonIntToID(rep[1]);
        else { //get the Control enum value
            Control control = WiiControl.ordinalToControl(rep[1]);
            return "(" + type.name + ")" + control.id;
        }
    }

    public static Object parseWiiControl(String prop, HashMap<String,String> properties) throws IllegalArgumentException {
        // syntax: (type)id

        int typeidx = prop.indexOf('(')+1;
        int typeend = prop.indexOf(')');
        if (typeidx==0 || typeend==-1) throw new IllegalArgumentException("Invalid wiimote trigger: " + prop + " (syntax: ([type])[id] )");
        String type = prop.substring(typeidx, typeend);
        int ididx = typeend+1;
        if (ididx>=prop.length()) throw new IllegalArgumentException("Invalid wiimote trigger: " + prop + " (syntax: ([type])[id] )");
        String id = prop.substring(ididx);
        int iid;
        Type repType = Type.parseType(type);
        if (repType==Type.BUTTON) iid = WiiControl.buttonIDToInt(id);
        else iid = Control.parseId(id).ordinal();

        return new int[] {repType.ordinal(), iid};
    }

}


